namespace ScrumSprintMonitor.Implementation.Configuration
{
	#region #using Directives

	using System;
	using System.Collections;
	using System.Globalization;
	using System.IO;
	using System.Linq;
	using System.Reflection;
	using System.Text;

	using Infrastructure;
	using Infrastructure.Configuration;

	#endregion

	public class StreamBasedSprintConfiguration : DataStreamObjectSerializer,
	                                              ISprintConfiguration
	{
		private readonly ILogger<StreamBasedSprintConfiguration> logger;

		public StreamBasedSprintConfiguration(IDataStreamProvider streamProvider, ILogger<StreamBasedSprintConfiguration> logger)
			: base(streamProvider)
		{
			this.logger = logger;
			SprintStartTimeZone = TimeZoneInfo.Utc;
			SprintEndTimeZone = TimeZoneInfo.Utc;

			EnsureObjectIsRead();
		}

		protected override void ReadObjectFromStream()
		{
			GuardDisposed();

			try
			{
				using (var stream = this.streamProvider.OpenStream(false))
				{
					using (var reader = new StreamReader(stream))
					{
						for (;;)
						{
							string line = reader.ReadLine();
							if (line == null)
							{
								break;
							}

							line = line.TrimStart();
							if (line.StartsWith("'"))
							{
								continue;
							}

							var tokens = line.Split(new[] {'='});
							string propertyName = tokens[0].Trim();

							if (propertyName == "BuildDefinitionName")
							{
								propertyName = "BuildDefinitionNames";
							}

							var propertyInfo = GetType().GetProperty(propertyName,
							                                         BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.DeclaredOnly);
							if (propertyInfo == null)
							{
								this.logger.Warn("Could not find property {0} in {1}. Skipping...", propertyName, GetType().ToString());
							}
							else if (!propertyInfo.CanWrite)
							{
								this.logger.Warn("Property {0} in {1} is not writable! Skipping...", propertyName, GetType().ToString());
							}
							else
							{
								string stringValue = tokens[1].Trim();

								try
								{
									propertyInfo.SetValue(this, GetValueFromString(propertyInfo.PropertyType, stringValue), null);
								}
								catch (FormatException e)
								{
									this.logger.Error(e, "Exception detected while reading sprint configuration parameter value {0}={1}.", propertyInfo.Name, stringValue);

									throw new Exception(
										string.Format("'{0}' is not a valid value for {1}.\n\nError found in file {2}.", stringValue, propertyInfo.Name,
										              this.streamProvider.Location), e);
								}
							}
						}
					}
				}
			}
			catch (IOException e)
			{
				this.logger.Warn(e, "Exception detected while reading sprint configuration.");

				throw;
			}

			AsyncRaisePropertyChanged(null); // This may get called from a non-UI thread
		}

		private static object GetValueFromString(Type valueType, string value)
		{
			object newValue;
			var formatProvider = GetInvariantFormatProvider(valueType);

			if (valueType.IsAssignableFrom(typeof(TimeZoneInfo)))
			{
				newValue = TimeZoneInfo.FindSystemTimeZoneById(value);
			}
			else if (valueType.BaseType == typeof(Enum))
			{
				newValue = Enum.Parse(valueType, value);
			}
			else if (valueType.BaseType == typeof(Array))
			{
				IList valuesList;
				if (string.IsNullOrEmpty(value))
				{
					valuesList = Array.CreateInstance(valueType.GetElementType(), 0);
				}
				else
				{
					var stringValues = value.Split(new[] {','});
					valuesList = Array.CreateInstance(valueType.GetElementType(), stringValues.Length);

					int i = 0;
					foreach (string stringValue in stringValues)
					{
						valuesList[i++] = GetValueFromString(valueType.GetElementType(), stringValue);
					}
				}

				newValue = valuesList;
			}
			else
			{
				newValue = Convert.ChangeType(value, valueType, formatProvider);
			}

			return newValue;
		}

		private static string WriteValueToString(Type valueType, object value)
		{
			string newValue;
			var formatProvider = GetInvariantFormatProvider(valueType);

			if (value == null)
			{
				newValue = string.Empty;
			}
			else if (valueType.BaseType == typeof(Array))
			{
				var stringValues = new StringBuilder();
				var valuesList = (IList) value;

				foreach (var item in valuesList)
				{
					if (stringValues.Length != 0)
					{
						stringValues.Append(",");
					}
					stringValues.Append(WriteValueToString(valueType.GetElementType(), item));
				}

				newValue = stringValues.ToString();
			}
			else
			{
				if (value is TimeZoneInfo)
				{
					var timeZoneInfo = (TimeZoneInfo) value;
					value = timeZoneInfo.Id;
				}

				newValue = Convert.ToString(value, formatProvider);
			}

			return newValue;
		}

		private static IFormatProvider GetInvariantFormatProvider(Type type)
		{
			if (type == typeof(DateTime) || type == typeof(TimeSpan))
			{
				return CultureInfo.InvariantCulture.DateTimeFormat;
			}
			if (type == typeof(double) || type == typeof(int))
			{
				return CultureInfo.InvariantCulture.NumberFormat;
			}

			return null;
		}

		#region Implementation of ISprintConfiguration

// ReSharper disable UnusedAutoPopertyAccessor.Global
		public string ProjectName { get; set; }
		public string ProcessTemplateName { get; set; }
		public int IterationId { get; set; }
		public string IterationPath { get; set; }
		public TimeZoneInfo SprintStartTimeZone { get; set; }
		public DateTime SprintStartDate { get; set; }
		public DateTime SprintEndDate { get; set; }
		public TimeZoneInfo SprintEndTimeZone { get; set; }

		public string[] BuildDefinitionNames { get; set; }

		public bool IsReadOnly
		{
			get
			{
				GuardDisposed();

				return this.streamProvider.IsReadOnly;
			}
		}

// ReSharper restore UnusedAutoPopertyAccessor.Global

		public void Save()
		{
			GuardDisposed();

			try
			{
				FreezeStreamChangeNotifications();

				using (var stream = this.streamProvider.OpenStream(true))
				{
					using (var writer = new StreamWriter(stream))
					{
						foreach (
							var propertyInfo in GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(p => p.CanWrite))
						{
							if (propertyInfo.Name == "IterationId")
							{
								continue;
							}

							var value = propertyInfo.GetValue(this, null);

							writer.WriteLine("{0}= {1}", propertyInfo.Name, WriteValueToString(propertyInfo.PropertyType, value));
						}
					}
				}
			}
			finally
			{
				UnfreezeStreamChangeNotifications();
			}
		}

		#endregion
	}
}